import utils from '../utils'
import { Bounds } from '../utils'
import { ArcIter, FilteredArcIter, ShapeIter } from './index.js'
import Geom from '../geom'
import simplify from '../simplify'
import Common from '../common.js'

/**
 * An interface for managing a collection of paths.
 *  Constructor signatures:
 * 
 * ArcCollection(arcs)
 *    arcs is an array of polyline arcs; each arc is an array of points: [[x0, y0], [x1, y1], ... ]
 * 
 * ArcCollection(nn, xx, yy)
 *    nn is an array of arc lengths; xx, yy are arrays of concatenated coords;
 */
function ArcCollection() {
  let _xx, _yy,  // coordinates data
      _ii, _nn,  // indexes, sizes
      _zz, _zlimit = 0, // simplification
      _bb, _allBounds, // bounding boxes
      _arcIter, _filteredArcIter // path iterators
  if (arguments.length == 1) {
    initLegacyArcs(arguments[0])  // want to phase this out
  } else if (arguments.length == 3) {
    initXYData.apply(this, arguments)
  } else {
    utils.error("ArcCollection() Invalid arguments")
  }
  function initLegacyArcs(arcs) {
    const xx = []
    const yy = []
    const nn = arcs.map((points) => {
      const n = points ? points.length : 0
      for (let i = 0; i < n; i += 1) {
        xx.push(points[i][0])
        yy.push(points[i][1])
      }
      return n
    })
    initXYData(nn, xx, yy)
  }
  function initXYData(nn, xx, yy) {
    const size = nn.length
    if (nn instanceof Array) nn = new Uint32Array(nn)
    if (xx instanceof Array) xx = new Float64Array(xx)
    if (yy instanceof Array) yy = new Float64Array(yy)
    _xx = xx
    _yy = yy
    _nn = nn
    _zz = null
    _zlimit = 0
    _filteredArcIter = null

    // generate array of starting idxs of each arc
    _ii = new Uint32Array(size)
    let idx = null
    let j = null
    for (idx = 0, j = 0; j < size; j += 1) {
      _ii[j] = idx
      idx += nn[j]
    }

    if (idx != _xx.length || _xx.length != _yy.length) {
      utils.error("ArcCollection#initXYData() Counting error")
    }

    initBounds()
    // Pre-allocate some path iterators for repeated use.
    _arcIter = new ArcIter(_xx, _yy)
    return this
  }
  function initZData(zz) {
    if (!zz) {
      _zz = null
      _zlimit = 0
      _filteredArcIter = null
    } else {
      if (zz.length != _xx.length) utils.error("ArcCollection#initZData() mismatched arrays")
      if (zz instanceof Array) zz = new Float64Array(zz)
      _zz = zz
      _filteredArcIter = new FilteredArcIter(_xx, _yy, _zz)
    }
  }
  function initBounds() {
    const data = calcArcBounds(_xx, _yy, _nn)
    _bb = data.bb
    _allBounds = data.bounds
  }
  function calcArcBounds(xx, yy, nn) {
    let numArcs = nn.length
    const bb = new Float64Array(numArcs * 4)
    const bounds = new Bounds()
    let arcOffs = 0
    let arcLen = null
    let j = null
    let b = null
    for (let i = 0; i < numArcs; i += 1) {
      arcLen = nn[i]
      if (arcLen > 0) {
        j = i * 4
        b = Geom.calcArcBounds(xx, yy, arcOffs, arcLen)
        bb[j++] = b[0]
        bb[j++] = b[1]
        bb[j++] = b[2]
        bb[j] = b[3]
        arcOffs += arcLen
        bounds.mergeBounds(b)
      }
    }
    return {
      bb: bb,
      bounds: bounds
    }
  }
  this.updateVertexData = function(nn, xx, yy, zz) {
    initXYData(nn, xx, yy)
    initZData(zz || null)
  }
  this.getVertexData = function() {
    return {
      xx: _xx,
      yy: _yy,
      zz: _zz,
      bb: _bb,
      nn: _nn,
      ii: _ii
    }
  }
  this.getCopy = function() {
    const copy = new ArcCollection(new Int32Array(_nn), new Float64Array(_xx),
        new Float64Array(_yy))
    if (_zz) {
      copy.setThresholds(new Float64Array(_zz))
      copy.setRetainedInterval(_zlimit)
    }
    return copy
  }
  function getFilteredPointCount() {
    const zz = _zz
    const z = _zlimit
    if (!zz || !z) return this.getPointCount()
    let count = 0;
    for (let i=0, n = zz.length; i < n; i += 1) {
      if (zz[i] >= z) count++
    }
    return count
  }
  function getFilteredVertexData() {
    const len2 = getFilteredPointCount()
    const arcCount = _nn.length
    const xx2 = new Float64Array(len2)
    const yy2 = new Float64Array(len2)
    const zz2 = new Float64Array(len2)
    const nn2 = new Int32Array(arcCount)
    let i=0
    let i2 = 0
    let n = null
    let n2 = null

    for (let arcId = 0; arcId < arcCount; arcId += 1) {
      n2 = 0
      n = _nn[arcId]
      for (let end = i + n; i < end; i += 1) {
        if (_zz[i] >= _zlimit) {
          xx2[i2] = _xx[i]
          yy2[i2] = _yy[i]
          zz2[i2] = _zz[i]
          i2++
          n2++
        }
      }
      if (n2 < 2) utils.error("Collapsed arc") // endpoints should be z == Infinity
      nn2[arcId] = n2
    }
    return {
      xx: xx2,
      yy: yy2,
      zz: zz2,
      nn: nn2
    }
  }
  this.getFilteredCopy = function() {
    if (!_zz || _zlimit === 0) return this.getCopy()
    const data = getFilteredVertexData()
    const copy = new ArcCollection(data.nn, data.xx, data.yy)
    copy.setThresholds(data.zz)
    return copy
  }
  this.toArray = function() {
    const arr = []
    this.forEach(function(iter) {
      const arc = []
      while (iter.hasNext()) {
        arc.push([iter.x, iter.y])
      }
      arr.push(arc)
    })
    return arr
  }
  this.toJSON = function() {
    return this.toArray()
  }
  this.forEachArcSegment = function(arcId, cb) {
    const fw = arcId >= 0
    const absId = fw ? arcId : ~arcId
    const zlim = this.getRetainedInterval()
    const n = _nn[absId]
    const step = fw ? 1 : -1
    let v1 = fw ? _ii[absId] : _ii[absId] + n - 1
    let v2 = v1
    const xx = _xx
    const yy = _yy
    const zz = _zz
    let count = 0

    for (let j = 1; j < n; j += 1) {
      v2 += step
      if (zlim === 0 || zz[v2] >= zlim) {
        cb(v1, v2, xx, yy)
        v1 = v2
        count++
      }
    }
    return count
  }
  this.forEachSegment = function(cb) {
    let count = 0
    for (let i=0, n=this.size(); i < n; i += 1) {
      count += this.forEachArcSegment(i, cb)
    }
    return count
  }
  this.transformPoints = function(f) {
    const xx = _xx
    const yy = _yy
    let arcId = -1
    let n = 0
    let p = null
    for (let i=0, len = xx.length; i < len; i++, n--) {
      while (n === 0) {
        n = _nn[++arcId]
      }
      p = f(xx[i], yy[i], arcId)
      if (p) {
        xx[i] = p[0]
        yy[i] = p[1]
      }
    }
    initBounds()
  }
  this.forEach = function(cb) {
    for (let i = 0, n = this.size(); i < n; i += 1) {
      cb(this.getArcIter(i), i)
    }
  }
  this.forEach2 = function(cb) {
    for (let arcId = 0, n = this.size(); arcId < n; arcId += 1) {
      cb(_ii[arcId], _nn[arcId], _xx, _yy, _zz, arcId)
    }
  }
  this.forEach3 = function(cb) {
    let start, end, xx, yy, zz
    for (let arcId = 0, n = this.size(); arcId < n; arcId += 1) {
      start = _ii[arcId]
      end = start + _nn[arcId]
      xx = _xx.subarray(start, end)
      yy = _yy.subarray(start, end)
      if (_zz) zz = _zz.subarray(start, end)
      cb(xx, yy, zz, arcId)
    }
  }
  this.filter = function(cb) {
    const test = function(i) {
      return cb(this.getArcIter(i), i)
    }.bind(this)
    return this.deleteArcs(test)
  }
  this.deleteArcs = function(test) {
    const n = this.size()
    const map = new Int32Array(n)
    let goodArcs = 0
    let goodPoints = 0
    for (let i = 0; i < n; i += 1) {
      if (test(i)) {
        map[i] = goodArcs++
        goodPoints += _nn[i]
      } else {
        map[i] = -1
      }
    }
    if (goodArcs < n) {
      condenseArcs(map)
    }
    return map
  }
  function condenseArcs(map) {
    let goodPoints = 0
    let goodArcs = 0
    const copyElements = utils.copyElements
    let k = null
    let arcLen = null
    for (let i = 0, n = map.length; i < n; i += 1) {
      k = map[i]
      arcLen = _nn[i]
      if (k > -1) {
        copyElements(_xx, _ii[i], _xx, goodPoints, arcLen)
        copyElements(_yy, _ii[i], _yy, goodPoints, arcLen)
        if (_zz) copyElements(_zz, _ii[i], _zz, goodPoints, arcLen)
        _nn[k] = arcLen
        goodPoints += arcLen
        goodArcs++
      }
    }

    initXYData(_nn.subarray(0, goodArcs), _xx.subarray(0, goodPoints),
        _yy.subarray(0, goodPoints))
    if (_zz) initZData(_zz.subarray(0, goodPoints))
  }
  this.dedupCoords = function() {
    let arcId = 0
    let i = 0
    let i2 = 0
    const arcCount = this.size()
    const zz = _zz
    let arcLen = null
    let arcLen2 = null
    while (arcId < arcCount) {
      arcLen = _nn[arcId]
      arcLen2 = dedupArcCoords(i, i2, arcLen, _xx, _yy, zz)
      _nn[arcId] = arcLen2
      i += arcLen
      i2 += arcLen2
      arcId++
    }
    if (i > i2) {
      initXYData(_nn, _xx.subarray(0, i2), _yy.subarray(0, i2))
      if (zz) initZData(zz.subarray(0, i2))
    }
    return i - i2
  }
  this.getVertex = function(arcId, nth) {
    const i = this.indexOfVertex(arcId, nth)
    return {
      x: _xx[i],
      y: _yy[i]
    }
  }
  // @nth: index of vertex. ~(idx) starts from the opposite endpoint
  this.indexOfVertex = function(arcId, nth) {
    const absId = arcId < 0 ? ~arcId : arcId
    const len = _nn[absId]
    if (nth < 0) nth = len + nth
    if (absId != arcId) nth = len - nth - 1
    if (nth < 0 || nth >= len) utils.error("[ArcCollection] out-of-range vertex id")
    return _ii[absId] + nth
  }
  // Test whether the vertex at index @idx is the endpoint of an arc
  this.pointIsEndpoint = function(idx) {
    const ii = _ii
    const nn = _nn
    for (let j = 0, n = ii.length; j < n; j += 1) {
      if (idx === ii[j] || idx === ii[j] + nn[j] - 1) return true
    }
    return false
  }
  // Tests if arc endpoints have same x, y coords
  // (arc may still have collapsed);
  this.arcIsClosed = function(arcId) {
    const i = this.indexOfVertex(arcId, 0)
    const j = this.indexOfVertex(arcId, -1)
    return i != j && _xx[i] == _xx[j] && _yy[i] == _yy[j]
  }
  // Tests if first and last segments mirror each other
  // A 3-vertex arc with same endpoints tests true
  this.arcIsLollipop = function(arcId) {
    const len = this.getArcLength(arcId)
    if (len <= 2 || !this.arcIsClosed(arcId)) return false
    const i = this.indexOfVertex(arcId, 1)
    const j = this.indexOfVertex(arcId, -2)
    return _xx[i] == _xx[j] && _yy[i] == _yy[j]
  }
  this.arcIsDegenerate = function(arcId) {
    const iter = this.getArcIter(arcId)
    let i = 0
    let x = null
    let y = null
    while (iter.hasNext()) {
      if (i > 0) {
        if (x != iter.x || y != iter.y) return false
      }
      x = iter.x
      y = iter.y
      i++
    }
    return true
  }
  this.getArcLength = function(arcId) {
    return _nn[Common.absArcId(arcId)]
  }
  this.getArcIter = function(arcId) {
    const fw = arcId >= 0
    const i = fw ? arcId : ~arcId
    const iter = _zz && _zlimit ? _filteredArcIter : _arcIter
    if (i >= _nn.length) {
      utils.error("#getArcId() out-of-range arc id:", arcId)
    }
    return iter.init(_ii[i], _nn[i], fw, _zlimit)
  }
  this.getShapeIter = function(ids) {
    return new ShapeIter(this).init(ids)
  }
  // Add simplification data to the dataset
  // @thresholds is either a single typed array or an array of arrays of removal thresholds for each arc;
  //
  this.setThresholds = function(thresholds) {
    const n = this.getPointCount()
    let zz = null
    if (!thresholds) {
      // nop
    } else if (thresholds.length == n) {
      zz = thresholds
    } else if (thresholds.length == this.size()) {
      zz = flattenThresholds(thresholds, n)
    } else {
      utils.error("Invalid threshold data")
    }
    initZData(zz)
    return this
  }
  function flattenThresholds(arr, n) {
    const zz = new Float64Array(n)
    let i = 0
    arr.forEach((arr) => {
      for (let j = 0, n = arr.length; j < n; i++, j++) {
        zz[i] = arr[j]
      }
    })
    if (i != n) utils.error("Mismatched thresholds")
    return zz
  }
  // bake in current simplification level, if any
  this.flatten = function() {
    if (_zlimit > 0) {
      const data = getFilteredVertexData()
      this.updateVertexData(data.nn, data.xx, data.yy)
      _zlimit = 0
    } else {
      _zz = null
    }
  }
  this.getRetainedInterval = function() {
    return _zlimit
  }
  this.setRetainedInterval = function(z) {
    _zlimit = z
    return this
  }
  this.getRetainedPct = function() {
    return this.getPctByThreshold(_zlimit)
  }
  this.setRetainedPct = function(pct) {
    if (pct >= 1) {
      _zlimit = 0
    } else {
      _zlimit = this.getThresholdByPct(pct)
      _zlimit = utils.clampIntervalByPct(_zlimit, pct)
    }
    return this
  }
  // Return array of z-values that can be removed for simplification
  //
  this.getRemovableThresholds = function(nth) {
    if (!_zz) utils.error("[arcs] Missing simplification data.")
    const skip = nth | 1
    const arr = new Float64Array(Math.ceil(_zz.length / skip))
    let z = null
    for (let i=0, j=0, n=this.getPointCount(); i<n; i+=skip) {
      z = _zz[i]
      if (z != Infinity) {
        arr[j++] = z
      }
    }
    return arr.subarray(0, j)
  }
  this.getArcThresholds = function(arcId) {
    if (!(arcId >= 0 && arcId < this.size())) {
      utils.error("[arcs] Invalid arc id:", arcId)
    }
    const start = _ii[arcId]
    const end = start + _nn[arcId]
    return _zz.subarray(start, end)
  }
  // nth (optional): sample every nth threshold (use estimate for speed)
  this.getPctByThreshold = function(val, nth) {
    let arr, rank, pct
    if (val > 0) {
      arr = this.getRemovableThresholds(nth)
      rank = utils.findRankByValue(arr, val)
      pct = arr.length > 0 ? 1 - (rank - 1) / arr.length : 1
    } else {
      pct = 1
    }
    return pct
  }
  // nth (optional): sample every nth threshold (use estimate for speed)
  this.getThresholdByPct = function(pct, nth) {
    return simplify.getThresholdByPct(pct, this, nth)
  }
  this.arcIntersectsBBox = function(i, b1) {
    const b2 = _bb
    const j = i * 4
    return b2[j] <= b1[2] && b2[j+2] >= b1[0] && b2[j+3] >= b1[1] && b2[j+1] <= b1[3]
  }
  this.arcIsContained = function(i, b1) {
    const b2 = _bb
    const j = i * 4
    return b2[j] >= b1[0] && b2[j+2] <= b1[2] && b2[j+1] >= b1[1] && b2[j+3] <= b1[3]
  }
  this.arcIsSmaller = function(i, units) {
    const bb = _bb
    const j = i * 4
    return bb[j+2] - bb[j] < units && bb[j+3] - bb[j+1] < units
  }
  // TODO: allow datasets in lat-lng coord range to be flagged as planar
  this.isPlanar = function() {
    return !Common.probablyDecimalDegreeBounds(this.getBounds())
  }
  this.size = function() {
    return _ii && _ii.length || 0
  }
  this.getPointCount = function() {
    return _xx && _xx.length || 0
  }
  this.getFilteredPointCount = getFilteredPointCount
  this.getBounds = function() {
    return _allBounds.clone()
  }
  this.getSimpleShapeBounds = function(arcIds, bounds) {
    bounds = bounds || new Bounds()
    for (let i=0, n=arcIds.length; i<n; i++) {
      this.mergeArcBounds(arcIds[i], bounds)
    }
    return bounds
  }
  this.getSimpleShapeBounds2 = function(arcIds, arr) {
    const bbox = arr || []
    const bb = _bb
    let id = Common.absArcId(arcIds[0]) * 4
    bbox[0] = bb[id]
    bbox[1] = bb[++id]
    bbox[2] = bb[++id]
    bbox[3] = bb[++id]
    for (let i=1, n=arcIds.length; i<n; i++) {
      id = Common.absArcId(arcIds[i]) * 4
      if (bb[id] < bbox[0]) bbox[0] = bb[id]
      if (bb[++id] < bbox[1]) bbox[1] = bb[id]
      if (bb[++id] > bbox[2]) bbox[2] = bb[id]
      if (bb[++id] > bbox[3]) bbox[3] = bb[id]
    }
    return bbox
  }
  // TODO: move this and similar methods out of ArcCollection
  this.getMultiShapeBounds = function(shapeIds, bounds) {
    bounds = bounds || new Bounds()
    if (shapeIds) { // handle null shapes
      for (let i=0, n=shapeIds.length; i<n; i++) {
        this.getSimpleShapeBounds(shapeIds[i], bounds)
      }
    }
    return bounds
  }
  this.mergeArcBounds = function(arcId, bounds) {
    if (arcId < 0) arcId = ~arcId
    const offs = arcId * 4
    bounds.mergeBounds(_bb[offs], _bb[offs+1], _bb[offs+2], _bb[offs+3])
  }
}
function dedupArcCoords(src, dest, arcLen, xx, yy, zz) {
  let n = 0
  let n2 = 0 // counters
  let x, y, i, j, keep
  while (n < arcLen) {
    j = src + n
    x = xx[j]
    y = yy[j]
    keep = x == x && y == y && (n2 === 0 || x != xx[j-1] || y != yy[j-1])
    if (keep) {
      i = dest + n2
      xx[i] = x
      yy[i] = y
      n2++
    }
    if (zz && n2 > 0 && (keep || zz[j] > zz[i])) {
      zz[i] = zz[j]
    }
    n++
  }
  return n2 > 1 ? n2 : 0
}

export default ArcCollection
